/*
  基于WebSocket和WebRTC的多用户聊天示例与双向视频
  这个文件包含实现服务器端的JavaScript代码
  聊天系统的功能，包括用户ID管理，消息
  反射和路由私有消息，包括对通过未知的JSON对象发送，以支持自定义应用程序和信号
*/
"use strict";

//var http = require('http');
var https = require('https');
var fs = require('fs');
var WebSocketServer = require('websocket').server;

// 用于管理文本聊天用户列表。
var connectionArray = [];
var nextID = Date.now();
var appendToMakeUnique = 1;

/*
  密钥
  加载用于HTTPS/WSS服务器的密钥和证书数据。
*/
var httpsOptions = {
  key: fs.readFileSync("./fake-keys/privatekey.pem"),
  cert: fs.readFileSync("./fake-keys/certificate.pem")
};

/*
  HTTPS服务器只提供WebSocket连接服务，所以每个请求只返回404。
  真正的Web请求由机器上的主服务器处理。
  如果愿意，可以在这里返回真正的HTML并提供Web内容。
  这里使用 express 来指定静态文件
*/
var express = require('express'), //引入express模块
    app = express();
app.use('/', express.static(__dirname + '/client')); //指定静态HTML文件的位置
var httpsServer = https.createServer(httpsOptions, app);

/*
  在分配给此示例的端口上启动HTTPS服务器。
  这将很快转换为WebSocket端口。
*/
httpsServer.listen(6503, function() {
  log("Server is listening on port 6503");
});

/*
  socket 服务器
  通过将HTTPS服务器转换为一个WebSocket服务器
  来创建WebSocket服务器。
*/
var wsServer = new WebSocketServer({
  httpServer: httpsServer,
  autoAcceptConnections: false
});
if (!wsServer) {
  log("错误:无法创建WbeSocket服务器!");
}

/*
  客户端连接
  在WebSocket服务器上设置一个“connect”消息处理程序。
  每当用户使用WebSocket协议连接到服务器端口时，都会调用这个函数。
*/
wsServer.on('request', function(request) {
  if (!originIsAllowed(request.origin)) {
    request.reject();
    log("Connection from " + request.origin + " rejected.");
    return;
  }
  // 接受请求并获得连接。
  var connection = request.accept("json", request.origin);
  // 将新连接添加到连接列表中。
  log("Connection accepted from " + connection.remoteAddress + ".");
  connectionArray.push(connection);
  connection.clientID = nextID;
  nextID++;
  // 向新客户端发送令牌;它返回一个“用户名”消息，告诉我们他们想使用什么用户名。
  var msg = {
    type: "id",
    id: connection.clientID
  };
  connection.sendUTF(JSON.stringify(msg));
  // 为通过WebSocket接收的“message”事件设置一个处理程序。这是客户端发送的消息，可以是与其他用户共享的文本、一个用户的私有消息(文本或信令)或到服务器的命令。
  connection.on('message', function(message) {
    if (message.type === 'utf8') {
      log("Received Message: " + message.utf8Data);
      // 处理传入的数据。
      var sendToClients = true;
      msg = JSON.parse(message.utf8Data);
      var connect = getConnectionForID(msg.id);
      // 查看传入对象并根据其类型对其进行操作。传递未知的消息类型，因为它们可能用于实现客户端特性。带有“target”属性的消息仅通过该名称发送给用户。
      switch(msg.type) {
        // 公共、文本消息
        case "message":
          msg.name = connect.username;
          msg.text = msg.text.replace(/(<([^>]+)>)/ig, "");
          break;
        // 用户名更改
        case "username":
          var nameChanged = false;
          var origName = msg.name;
          // 如果名称不是惟一的，则在名称后面附加一个数字，以确保名称是惟一的
          while (!isUsernameUnique(msg.name)) {
            msg.name = origName + appendToMakeUnique;
            appendToMakeUnique++;
            nameChanged = true;
          }
          // 如果必须更改名称，我们将向用户发送一条“rejectusername”消息，让他们知道自己的名称已被服务器更改。
          if (nameChanged) {
            var changeMsg = {
              id: msg.id,
              type: "rejectusername",
              name: msg.name
            };
            connect.sendUTF(JSON.stringify(changeMsg));
          }
          /*
            设置此连接的最终用户名，并将更新后的用户列表发送给所有用户。
            是的，我们正在发送一个完整的列表，而不是仅仅更新。
            这是一个非常低效的演示。不要在真正的应用程序中这样做。
          */
          connect.username = msg.name;
          sendUserListToAll();
          sendToClients = false;  // 我们已经发出了正确的回应
          break;
      }
      /*
        如果是消息
        将修改后的消息转换回JSON，并将其发送到指定的客户机或所有客户机(视情况而定)。
        我们传递上面select块中没有特别处理的任何消息。
        这允许客户端不受阻碍地交换信令和其他控制对象。
      */
      if (sendToClients) {
        var msgString = JSON.stringify(msg);
        var i;
        // 如果消息指定了目标用户名，则只将消息发送给他们。否则，将其发送给每个用户。
        if (msg.target && msg.target !== undefined && msg.target.length !== 0) {
          sendToOneUser(msg.target, msgString);
        } else {
          for (i=0; i<connectionArray.length; i++) {
            connectionArray[i].sendUTF(msgString);
          }
        }
      }
    }
  });
  // 处理WebSocket“关闭”事件;这意味着用户已注销或已断开连接。
  connection.on('close', function(reason, description) {
    // 首先，从连接列表中删除连接。
    connectionArray = connectionArray.filter(function(el, idx, ar) {
      return el.connected;
    });
    // 现在发送更新后的用户列表。同样，请不要在实际应用程序中这样做。你的用户不会很喜欢你。
    sendUserListToAll();
    // 为关闭信息构建和输出日志输出。
    var logMessage = "Connection closed: " + connection.remoteAddress + " (" + reason;
    if (description !== null && description.length !== 0) {
      logMessage += ": " + description;
    }
    logMessage += ")";
    log(logMessage);
  });
});

/*
  向所有聊天成员发送“userlist”消息。
  这是一种俗气的方法，可以确保每个join/drop都在任何地方得到反映。
  向每个用户发送简单的join/drop消息会更有效
  但是对于这个简单的示例来说，这已经足够了。
*/
function sendUserListToAll() {
  var userListMsg = makeUserListMessage();
  var userListMsgStr = JSON.stringify(userListMsg);
  var i;
  for (i=0; i<connectionArray.length; i++) {
    connectionArray[i].sendUTF(userListMsgStr);
  }
}

/*
  构建类型为“userlist”的消息对象，其中包含所有已连接用户的名称。
  用于启动新登录的用户，并低效地处理名称更改通知。
*/
function makeUserListMessage() {
  var userListMsg = {
    type: "userlist",
    users: []
  };
  var i;
  // 将用户添加到列表中
  for (i=0; i<connectionArray.length; i++) {
    userListMsg.users.push(connectionArray[i].username);
  }
  return userListMsg;
}

/*
  扫描用户列表，看看指定的名称是否惟一。
  如果是，返回true。否则,返回false。
  我们希望所有用户都有唯一的名称。
*/
function isUsernameUnique(name) {
  var isUnique = true;
  var i;
  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].username === name) {
      isUnique = false;
      break;
    }
  }
  return isUnique;
}

/*
  给定单个用户的用户名，向该用户发送一条消息(该消息已经是字符串化的JSON)。
  我们将其用于WebRTC信令，也可以用于私有文本消息。
*/
function sendToOneUser(target, msgString) {
  var isUnique = true;
  var i;
  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].username === target) {
      connectionArray[i].sendUTF(msgString);
      break;
    }
  }
}

/*
  扫描连接列表并返回指定clientID的连接。
  每个登录都获得一个在会话期间不会更改的ID，因此可以跨用户名更改跟踪它。
*/
function getConnectionForID(id) {
  var connect = null;
  var i;
  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].clientID === id) {
      connect = connectionArray[i];
      break;
    }
  }
  return connect;
}

/*
  如果您想实现对阻塞特定源的支持，可以从这里开始。
  只要返回false以拒绝给定指定源的WebSocket连接。
*/
function originIsAllowed(origin) {
  return true;    // 我们接受所有的连接
}

// 将日志信息输出到控制台
function log(text) {
  var time = new Date();
  console.log("[" + time.toLocaleTimeString() + "] " + text);
}